import * as THREE from "three";
import { EventDispatcher, Raycaster, Object3D, Scene, Vector2 } from 'three';
import { CSS3DSprite, CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'

EventDispatcher.prototype.on = function (type, fn) {
    if (this instanceof Object3D && !['added', 'removed', 'dispose'].includes(type)) this.interactive = true
    this.addEventListener(type, fn)

    if (this instanceof CSS3DSprite || this instanceof CSS3DObject || this instanceof CSS2DObject) {

        this.element.addEventListener("click", (ev) => {
            this.emit('click', this, ev)
        })

        this.element.addEventListener("dblclick", (ev) => {
            this.emit('dblclick', this, ev)
        })

        this.element.addEventListener("mousemove", (ev) => {
            this.emit('hover', this, ev)
            this.element.style['cursor'] = this.cursor_hover || 'pointer';
        })

        this.element.addEventListener("mouseleave", (ev) => {
            this.emit('hoverleave', this, ev)
            this.element.style['cursor'] = this.cursor_hoverleave || 'auto';
        })
    }
}

EventDispatcher.prototype.off = function (type, fn) {
    if (fn instanceof Function) {
        this.removeEventListener(type, fn)
    } else {
        this._listeners && this._listeners[type] && delete this._listeners[type]
    }
}

EventDispatcher.prototype.emit = function (type, ...argument) {

    if (this._listeners === undefined || !this._listeners[type]) return;
    let cbs = this._listeners[type] || [];

    let cache = cbs.slice(0);

    for (let i = 0; i < cache.length; i++) {
        cache[i].apply(this, argument)
    }
}

EventDispatcher.prototype.onLifeCycle = function (removed, added) {
    if (added instanceof Function) {
        this.addEventListener('added', added)
    }

    if (removed instanceof Function) {
        this.addEventListener('removed', removed)
    }
}

EventDispatcher.prototype.offLifeCycle = function (removed, added) {
    this.off('added', added)
    this.off('removed', removed)
}


class RaycasterEvent {
    constructor(scene, camera, renderer, config = {}) {
        this.scene = scene
        this.camera = camera
        this.renderer = renderer

        let { cursor = {}, mousePoint = {} } = config
        this.cursor = Object.assign({ nor: 'auto', hover: "pointer" }, cursor)

        this.mousePointConf = Object.assign({ enable: false, texture: undefined, size: [30, 30], color: 'white', opacity: 1, scalar: 0.4, insideNear: 0, waypointIndoorRadius: 0.24 }, mousePoint)


        this.raycaster = new Raycaster();

        this.click = this.click.bind(this)
        this.dblclick = this.dblclick.bind(this)
        this.hover = this.hover.bind(this)

        this.addEvents()


        if (this.mousePointConf.enable) {

            let geometry = new THREE.PlaneGeometry(...this.mousePointConf.size)
            let material = new THREE.RawShaderMaterial({
                side: THREE.DoubleSide,
                depthWrite: false,
                depthTest: false,
                transparent: true,
                vertexShader: "precision highp float;\nprecision highp int;\n\nuniform mat4 modelMatrix;\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 cameraPosition;\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec2 uv;\n"
                    + "varying vec2 vUv;\nvarying vec4 vPointView;\n\nvoid main() {\n\n  vUv = uv;\n  vPointView = modelViewMatrix * vec4( position, 1.0 );\n  gl_Position = projectionMatrix * vPointView;\n\n}\n",
                fragmentShader: "precision highp float;\nprecision highp int;\n\nuniform mat4 viewMatrix;\nuniform vec3 cameraPosition;\n" + "uniform sampler2D map;\nuniform float opacity; // another opacity, with a different clock\nuniform vec2 nearFade;\nvarying vec2 vUv;\nvarying vec4 vPointView;\nuniform vec3 color;\n\nvoid main() {\n\t// TODO add scroll-in and pulsing behaviors\n\tfloat depthFade = min(1.0, (abs(vPointView.z)-nearFade.x)/(nearFade.y-nearFade.x));\n  vec4 colorFromTexture = texture2D( map, vUv );\t\t// we only use the alpha!\n  gl_FragColor = vec4((color.rgb*colorFromTexture.rgb),\n  \t\t\t\t\t\t(opacity*colorFromTexture.a * depthFade));\n}\n",
                name: "mousePoint",
                opacity: 0,
                uniforms: {
                    map: {
                        type: "float",
                        value: this.mousePointConf.texture || new THREE.TextureLoader().load(require('./pointer.png'))
                    },
                    opacity: {
                        type: "float",
                        value: this.mousePointConf.opacity
                    },
                    nearFade: {
                        type: "vec2",
                        value: new THREE.Vector2(2 * this.mousePointConf.insideNear, 2 * this.mousePointConf.waypointIndoorRadius)
                    },
                    color: {
                        type: "color",
                        value: new THREE.Color(this.mousePointConf.color)
                    }
                },
            })

            // let material2 = new THREE.MeshBasicMaterial({
            // 	side: THREE.DoubleSide,
            // 	transparent: true,
            // 	depthWrite: false,
            // 	depthTest: false,
            // 	map: new THREE.TextureLoader().load(require('./pointer.png'))
            // })

            this.mousePoint = new THREE.Mesh(geometry, material);
            this.mousePoint.name = 'mouse-point-mesh'
            this.mousePoint.visible = false
            this.mousePoint.renderOrder = 3

            scene.add(this.mousePoint);
        }

    }

    addEvents() {
        //在window事件不会被阻止
        // window.addEventListener('click', this.click, false);
        // window.addEventListener('mousemove', this.hover, false);
        this.renderer.domElement.addEventListener('click', this.click, true);
        this.renderer.domElement.addEventListener('dblclick', this.dblclick, true);//dblclick 在移动端可能不生效
        this.renderer.domElement.addEventListener('mousemove', this.hover, true);
        this.renderer.domElement.addEventListener('mouseleave', () => {
            if (this.mousePoint) {
                this.mousePoint.visible = false
            }
        }, true);

    }

    click(event) {
        event.preventDefault();
        if (event.type !== 'click') return;
        this.clickTimeId && clearTimeout(this.clickTimeId);

        let intersects = this.intersect(event)
        let obj = this.findInteractive(intersects, 'click')
        if (obj) {
            let { object } = Object.assign(event, obj)
            if (object._listeners['dblclick']) {
                this.clickTimeId = setTimeout(function () {
                    object && object.emit('click', object, event)
                }, 250)
            } else {
                object && object.emit('click', object, event)
            }
        }
    }

    dblclick(event) {
        event.preventDefault();
        if (event.type !== 'dblclick') return;
        this.clickTimeId && clearTimeout(this.clickTimeId);

        let intersects = this.intersect(event)
        let obj = this.findInteractive(intersects, 'dblclick')
        if (obj) {
            let { object } = Object.assign(event, obj)
            object && object.emit('dblclick', object, event)
        }
    }

    hover(event) {
        event.preventDefault();
        if (event.type !== 'mousemove') return;

        let intersects = this.intersect(event)

        if (this.mousePointConf.enable && this.mousePoint && intersects.length) {
            let obj = intersects.find(i => {
                return i.face && i.object.name !== 'mouse-point-circle' && i.object.id != this.mousePoint.id
            });

            if (obj) {
                let normal = obj.face.normal.clone().multiplyScalar(this.mousePointConf.scalar)
                let position = obj.point.clone().add(normal)
                this.mousePoint.position.copy(position)
                let lookAt = position.clone().add(normal)
                this.mousePoint.lookAt(lookAt)


                if (.99 < Math.abs(obj.face.normal.y)) {
                    let l = this.camera.position.clone().sub(position);
                    this.mousePoint.rotation.z = -Math.atan2(l.z, l.x)
                }

                this.mousePoint.visible = true
            } else {
                this.mousePoint.visible = false
            }

        } else {
            if (this.mousePoint) {
                this.mousePoint.visible = false
            }
        }


        if (this.findInteractive(intersects, 'all')) {
            this.renderer.domElement.style['cursor'] = this.cursor.hover
        } else {
            this.renderer.domElement.style['cursor'] = this.cursor.nor
        }

        let obj = this.findInteractive(intersects, 'hover')
        if (obj) {
            let { object } = Object.assign(event, obj)
            if (object != this.object) {
                this.object && this.object.emit('hoverleave', this.object, event)
                object && object.emit('hover', object, event)
                this.object = object
            }
            object && object.emit('hovermove', object, event)
        } else {
            this.object && this.object.emit('hoverleave', this.object, event)
            this.object = null
        }
    }
    intersect(event) {
        let rect = {
            left: 0,
            top: 0,
            width: window.innerWidth,
            height: window.innerHeight,
        }
        if (this.renderer) {
            rect = this.renderer.domElement.getBoundingClientRect()
        }
        this.raycaster.setFromCamera(new Vector2(((event.clientX - rect.left) / rect.width) * 2 - 1, - ((event.clientY - rect.top) / rect.height) * 2 + 1), this.camera);
        return this.raycaster.intersectObjects(this.scene.children, true);
    }

    findInteractive(intersects, event) {
        let interactive = (object) => {

            let flag = () => event === 'all' ? true : object._listeners[event]

            if (object.visible && object.interactive && flag()) {
                return object
            } else {
                if (object.parent && !(object.parent instanceof Scene)) {
                    return interactive(object.parent)
                } else {
                    return null
                }
            }
        }

        let obj;

        let map = (obj, intersect) => {
            return {
                object: obj,
                distance: intersect.distance,
                point: intersect.point,
                intersect: intersect
            }
        }

        if (intersects.length) {
            for (let i = 0; i < intersects.length; i++) {
                let intersect = intersects[i]
                obj = interactive(intersect.object)
                if (obj) {
                    return map(obj, intersect)
                }
            }

            obj = interactive(this.scene)
            if (obj) {
                let intersect = intersects[0]
                return map(obj, intersect)
            }

        }
        return null
    }
}
export default RaycasterEvent;
